// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ObjExporter.cs" company="Helix 3D Toolkit">
//   http://helixtoolkit.codeplex.com, license: Ms-PL
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace HelixToolkit
{
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Windows.Media;
    using System.Windows.Media.Media3D;

    /// <summary>
    /// Export the 3D visual tree to a Wavefront OBJ file

    /// </summary>
    /// <remarks>
    /// http://en.wikipedia.org/wiki/Obj
    ///   http://www.martinreddy.net/gfx/3d/OBJ.spec
    ///   http://www.eg-models.de/formats/Format_Obj.html
    /// </remarks>
    public class ObjExporter : Exporter
    {
        #region Constants and Fields

        /// <summary>
        ///   The directory.
        /// </summary>
        private readonly string directory;

        /// <summary>
        ///   The mwriter.
        /// </summary>
        private readonly StreamWriter mwriter;

        /// <summary>
        ///   The writer.
        /// </summary>
        private readonly StreamWriter writer;

        /// <summary>
        ///   The group no.
        /// </summary>
        private int groupNo = 1;

        /// <summary>
        ///   The mat no.
        /// </summary>
        private int matNo = 1;

        /// <summary>
        ///   Normal index counter.
        /// </summary>
        private int normalIndex = 1;

        /// <summary>
        ///   The object no.
        /// </summary>
        private int objectNo = 1;

        /// <summary>
        ///   Texture index counter.
        /// </summary>
        private int textureIndex = 1;

        /// <summary>
        ///   Vertex index counter.
        /// </summary>
        private int vertexIndex = 1;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ObjExporter"/> class.
        /// </summary>
        /// <param name="outputFileName">
        /// Name of the output file.
        /// </param>
        public ObjExporter(string outputFileName)
            : this(outputFileName, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ObjExporter"/> class.
        /// </summary>
        /// <param name="outputFileName">
        /// Name of the output file.
        /// </param>
        /// <param name="comment">
        /// The comment.
        /// </param>
        public ObjExporter(string outputFileName, string comment)
        {
            var fullPath = Path.GetFullPath(outputFileName);
            var mtlPath = Path.ChangeExtension(outputFileName, ".mtl");
            string mtlFilename = Path.GetFileName(mtlPath);
            this.directory = Path.GetDirectoryName(fullPath);

            this.writer = new StreamWriter(outputFileName);
            this.mwriter = new StreamWriter(mtlPath);

            if (!string.IsNullOrEmpty(comment))
            {
                this.writer.WriteLine(string.Format("# {0}", comment));
            }

            this.writer.WriteLine("mtllib " + mtlFilename);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Closes this exporter.
        /// </summary>
        public override void Close()
        {
            this.writer.Close();
            this.mwriter.Close();
            base.Close();
        }

        /// <summary>
        /// The export mesh.
        /// </summary>
        /// <param name="m">
        /// The m.
        /// </param>
        /// <param name="t">
        /// The t.
        /// </param>
        public void ExportMesh(MeshGeometry3D m, Transform3D t)
        {
            // mapping from local indices (0-based) to the obj file indices (1-based)
            var vertexIndexMap = new Dictionary<int, int>();
            var textureIndexMap = new Dictionary<int, int>();
            var normalIndexMap = new Dictionary<int, int>();

            int index = 0;
            foreach (var v in m.Positions)
            {
                vertexIndexMap.Add(index++, this.vertexIndex++);
                var p = t.Transform(v);
                this.writer.WriteLine(string.Format(CultureInfo.InvariantCulture, "v {0} {1} {2}", p.X, p.Y, p.Z));
            }

            index = 0;
            foreach (var vt in m.TextureCoordinates)
            {
				if (!double.IsNegativeInfinity(vt.X) && !double.IsPositiveInfinity(vt.Y))
				{
					textureIndexMap.Add(index++, this.textureIndex++);
					this.writer.WriteLine(string.Format(CultureInfo.InvariantCulture, "vt {0} {1}", vt.X, vt.Y));
				}
				else
				{
					index++;
				}
            }

            index = 0;
            foreach (var vn in m.Normals)
            {
				if (!double.IsNegativeInfinity(vn.X) && !double.IsPositiveInfinity(vn.Y))
				{
					normalIndexMap.Add(index++, this.normalIndex++);
					this.writer.WriteLine(string.Format(CultureInfo.InvariantCulture, "vn {0} {1} {2}", vn.X, vn.Y, vn.Z));
				}
				else
				{
					index++;
				}
            }

            const string formatString = "f {0}/{1}{2} {3}/{4}{5} {6}/{7}{8}";

            for (int i = 0; i < m.TriangleIndices.Count; i += 3)
            {
                int i0 = m.TriangleIndices[i];
                int i1 = m.TriangleIndices[i + 1];
                int i2 = m.TriangleIndices[i + 2];

                this.writer.WriteLine(
                    formatString,
                    vertexIndexMap[i0],
                    textureIndexMap.ContainsKey(i0) ? textureIndexMap[i0].ToString() : string.Empty,
                    normalIndexMap.ContainsKey(i0) ? "/"+normalIndexMap[i0].ToString() : string.Empty,
                    vertexIndexMap[i1],
                    textureIndexMap.ContainsKey(i1) ? textureIndexMap[i1].ToString() : string.Empty,
					normalIndexMap.ContainsKey(i1) ? "/" + normalIndexMap[i1].ToString() : string.Empty,
                    vertexIndexMap[i2],
                    textureIndexMap.ContainsKey(i2) ? textureIndexMap[i2].ToString() : string.Empty,
					normalIndexMap.ContainsKey(i2) ? "/" + normalIndexMap[i2].ToString() : string.Empty);
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// The export model.
        /// </summary>
        /// <param name="model">
        /// The model.
        /// </param>
        /// <param name="transform">
        /// The transform.
        /// </param>
        protected override void ExportModel(GeometryModel3D model, Transform3D transform)
        {
            this.writer.WriteLine(string.Format("o object{0}", this.objectNo++));
            this.writer.WriteLine(string.Format("g group{0}", this.groupNo++));
            string matName = string.Format("mat{0}", this.matNo++);
            this.writer.WriteLine(string.Format("usemtl {0}", matName));
            this.ExportMaterial(matName, model.Material, model.BackMaterial);
            var mesh = model.Geometry as MeshGeometry3D;
            this.ExportMesh(mesh, Transform3DHelper.CombineTransform(transform, model.Transform));
        }

        /// <summary>
        /// The export material.
        /// </summary>
        /// <param name="matName">
        /// The mat name.
        /// </param>
        /// <param name="material">
        /// The material.
        /// </param>
        /// <param name="backMaterial">
        /// The back material.
        /// </param>
        private void ExportMaterial(string matName, Material material, Material backMaterial)
        {
            this.mwriter.WriteLine(string.Format("newmtl {0}", matName));
            var dm = material as DiffuseMaterial;
            var sm = material as SpecularMaterial;
            var mg = material as MaterialGroup;
            if (mg != null)
            {
                foreach (var m in mg.Children)
                {
                    if (m is DiffuseMaterial)
                    {
                        dm = m as DiffuseMaterial;
                    }

                    if (m is SpecularMaterial)
                    {
                        sm = m as SpecularMaterial;
                    }
                }
            }

            if (dm != null)
            {
                this.mwriter.WriteLine(string.Format("Ka {0}", this.ToColorString(dm.AmbientColor)));
                var scb = dm.Brush as SolidColorBrush;
                if (scb != null)
                {
                    this.mwriter.WriteLine(string.Format("Kd {0}", this.ToColorString(scb.Color)));
                    this.mwriter.WriteLine(string.Format(CultureInfo.InvariantCulture, "d {0:F4}", scb.Color.A / 255.0));
                }
                else
                {
                    var textureFilename = matName + ".png";
                    var texturePath = Path.Combine(this.directory, textureFilename);

                    ImageBrush b = (ImageBrush)dm.Brush;
                    // create .png bitmap file for the brush
                    RenderBrush(texturePath, dm.Brush, (int)b.ImageSource.Width, (int)b.ImageSource.Height);
                    this.mwriter.WriteLine(string.Format("map_Kd {0}", textureFilename));
					this.mwriter.WriteLine(string.Format("map_d {0}", textureFilename));
                }
            }

            if (sm != null)
            {
                var scb = sm.Brush as SolidColorBrush;
                if (scb != null)
                {
                    this.mwriter.WriteLine(string.Format("Ks {0}", this.ToColorString(scb.Color)));
                }

                // todo: Shininess conversion to SpecularPower?
                this.mwriter.WriteLine(string.Format(CultureInfo.InvariantCulture, "Ns {0:F4}", sm.SpecularPower));
            }
        }

        /// <summary>
        /// Converts a color to a string.
        /// </summary>
        /// <param name="color">
        /// The color.
        /// </param>
        /// <returns>
        /// The string.
        /// </returns>
        private string ToColorString(Color color)
        {
            return string.Format(
                CultureInfo.InvariantCulture, "{0:F4} {1:F4} {2:F4}", color.R / 255.0, color.G / 255.0, color.B / 255.0);
        }

        #endregion
    }
}
